/*
  Copyright 2022-2023 Picovoice Inc.

  You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
  file accompanying this source.

  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  specific language governing permissions and limitations under the License.
*/

import {
  loadModel,
} from '@picovoice/web-utils';

import PvWorker from 'web-worker:./porcupine_worker_handler.ts';

import { keywordsProcess } from './utils';

import {
  DetectionCallback,
  PorcupineKeyword,
  PorcupineModel,
  PorcupineOptions,
  PorcupineWorkerInitResponse,
  PorcupineWorkerProcessResponse,
  PorcupineWorkerReleaseResponse,
  PvStatus
} from './types';
import { BuiltInKeyword } from './built_in_keywords';

import { pvStatusToException } from './porcupine_errors';

export class PorcupineWorker {
  private readonly _worker: Worker;
  private readonly _version: string;
  private readonly _frameLength: number;
  private readonly _sampleRate: number;

  private static _wasm: string;
  private static _wasmSimd: string;
  private static _sdk: string = "web";

  private constructor(worker: Worker, version: string, frameLength: number, sampleRate: number) {
    this._worker = worker;
    this._version = version;
    this._frameLength = frameLength;
    this._sampleRate = sampleRate;
  }

  /**
   * Get Porcupine engine version.
   */
  get version(): string {
    return this._version;
  }

  /**
   * Get Porcupine frame length.
   */
  get frameLength(): number {
    return this._frameLength;
  }

  /**
   * Get sample rate.
   */
  get sampleRate(): number {
    return this._sampleRate;
  }

  /**
   * Get Porcupine worker instance.
   */
  get worker(): Worker {
    return this._worker;
  }

  /**
   * Creates an instance of the Porcupine wake word engine using either
   * a '.pv' file in public directory or a base64'd string.
   * The model size is large, hence it will try to use the existing one
   * if it exists, otherwise saves the model in storage.
   *
   * @param accessKey AccessKey generated by Picovoice Console.
   * @param keywords - Built-in or Base64
   * representations of keywords and their sensitivities.
   * Can be provided as an array or a single keyword.
   * @param keywordDetectionCallback - User-defined callback invoked upon detection of the wake phrase.
   * The only input argument is the index of detected keyword (phrase).
   * @param model object containing a base64 string
   * representation of or path to public binary of a Porcupine parameter model used to initialize Porcupine.
   * @param model.base64 The model in base64 string to initialize Leopard.
   * @param model.publicPath The model path relative to the public directory.
   * @param model.customWritePath Custom path to save the model in storage.
   * Set to a different name to use multiple models across `Porcupine` instances.
   * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
   * @param model.version Leopard model version. Set to a higher number to update the model file.
   * @param options Optional configuration arguments.
   * @param options.processErrorCallback User-defined callback invoked if any error happens
   * while processing the audio stream. Its only input argument is the error message.
   *
   * @returns An instance of PorcupineWorker.
   */
  public static async create(
    accessKey: string,
    keywords: Array<PorcupineKeyword | BuiltInKeyword> | PorcupineKeyword | BuiltInKeyword,
    keywordDetectionCallback: DetectionCallback,
    model: PorcupineModel,
    options: PorcupineOptions = {},
  ): Promise<PorcupineWorker> {
    const [keywordPaths, keywordLabels, sensitivities] = await keywordsProcess(keywords);

    const customWritePath = (model.customWritePath) ? model.customWritePath : 'porcupine_model';
    const modelPath = await loadModel({ ...model, customWritePath });

    const { processErrorCallback, ...workerOptions } = options;

    const worker = new PvWorker();
    const returnPromise: Promise<PorcupineWorker> = new Promise((resolve, reject) => {
      // @ts-ignore - block from GC
      this.worker = worker;
      worker.onmessage = (event: MessageEvent<PorcupineWorkerInitResponse>): void => {
        switch (event.data.command) {
          case 'ok':
            worker.onmessage = (ev: MessageEvent<PorcupineWorkerProcessResponse>): void => {
              switch (ev.data.command) {
                case 'ok':
                  keywordDetectionCallback(ev.data.porcupineDetection);
                  break;
                case 'failed':
                case 'error':
                  const error = pvStatusToException(ev.data.status, ev.data.shortMessage, ev.data.messageStack);
                  if (processErrorCallback) {
                    processErrorCallback(error);
                  } else {
                    // eslint-disable-next-line no-console
                    console.error(error);
                  }
                  break;
                default:
                  // @ts-ignore
                  processErrorCallback(pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`));
              }
            };
            resolve(new PorcupineWorker(worker, event.data.version, event.data.frameLength, event.data.sampleRate));
            break;
          case 'failed':
          case 'error':
            const error = pvStatusToException(event.data.status, event.data.shortMessage, event.data.messageStack);
            reject(error);
            break;
          default:
            // @ts-ignore
            reject(pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`));
        }
      };
    });

    worker.postMessage({
      command: 'init',
      accessKey: accessKey,
      modelPath: modelPath,
      keywordPaths: keywordPaths,
      keywordLabels: keywordLabels,
      sensitivities: sensitivities,
      wasm: this._wasm,
      wasmSimd: this._wasmSimd,
      sdk: this._sdk,
      options: workerOptions,
    });

    return returnPromise;
  }

  /**
   * Set base64 wasm file.
   * @param wasm Base64'd wasm file to use to initialize wasm.
   */
  public static setWasm(wasm: string): void {
    if (this._wasm === undefined) {
      this._wasm = wasm;
    }
  }

  /**
   * Set base64 wasm file with SIMD feature.
   * @param wasmSimd Base64'd wasm file to use to initialize wasm.
   */
  public static setWasmSimd(wasmSimd: string): void {
    if (this._wasmSimd === undefined) {
      this._wasmSimd = wasmSimd;
    }
  }

  public static setSdk(sdk: string): void {
    PorcupineWorker._sdk = sdk;
  }

  /**
   * Processes a frame of audio in a worker.
   * The transcript result will be supplied with the callback provided when initializing the worker either
   * by 'fromBase64' or 'fromPublicDirectory'.
   * Can also send a message directly using 'this.worker.postMessage({command: "process", pcm: [...]})'.
   *
   * @param pcm A frame of audio sample.
   */
  public process(pcm: Int16Array): void {
    this._worker.postMessage({
      command: 'process',
      inputFrame: pcm,
    });
  }

  /**
   * Releases resources acquired by WebAssembly module.
   */
  public release(): Promise<void> {
    const returnPromise: Promise<void> = new Promise((resolve, reject) => {
      this._worker.onmessage = (event: MessageEvent<PorcupineWorkerReleaseResponse>): void => {
        switch (event.data.command) {
          case 'ok':
            resolve();
            break;
          case 'failed':
          case 'error':
            const error = pvStatusToException(event.data.status, event.data.shortMessage, event.data.messageStack);
            reject(error);
            break;
          default:
            // @ts-ignore
            reject(pvStatusToException(PvStatus.RUNTIME_ERROR, `Unrecognized command: ${event.data.command}`));
        }
      };
    });

    this._worker.postMessage({
      command: 'release',
    });

    return returnPromise;
  }

  /**
   * Terminates the active worker. Stops all requests being handled by worker.
   */
  public terminate(): void {
    this._worker.terminate();
  }
}
